package model.path;

import java.awt.Point;
import java.awt.geom.Line2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import model.csExceptions.NotEnoughGrenades;
import model.csExceptions.TooMuchGrenades;
import model.enumTypes.CSGrenadeType;
import model.enumTypes.CSMoveStyle;
import model.events.CSGrenadeEvent;
import model.events.CSLOSEvent;
import model.world.CSPlayer;
import parameters.GUIParameters;
import constants.CSConstants;
import constants.GUIConstants;





/**
 * This class implements the player's path model.
 * It also implements different jobs on the path.
 * 
 * @author Urbanek Lucas (aka NaG)
 *
 */
public class CSPlayerPath implements Serializable {
	
	private double endTime;
	private List<CSNode> nodes;
	private List<CSGrenadeEvent> grenadeEvents;
	private List<CSLOSEvent> losEvents;
	private double currentTime;
	
	// Very privates members :
	private double currentWaitTime;
	private Point currentPosition;
	private int currentNodeIndex;
	
	private CSPlayer player;
	
	/**
	 * This is the main constructor of the CSPlayerPath class.
	 * It needs the roundTime and the player's spawn point.
	 * 
	 * @param roundTime
	 * @param spawn
	 */
	public CSPlayerPath(double roundTime, Point spawn, CSPlayer player_) {
		
		endTime = roundTime;
		nodes = new ArrayList<CSNode>();
		grenadeEvents = new ArrayList<CSGrenadeEvent>();
		losEvents = new ArrayList<CSLOSEvent>();
		
		player = player_;
		
		// Adding the first node :
		nodes.add(new CSNode(spawn.x, spawn.y, 0, 0));
		
		currentTime = 0;
		currentPosition = spawn;
		currentNodeIndex = 0;
	}
	
	// End time :
	
	public double getEndTime() {
		return endTime;
	}
	public void setEndTime(double endTime) {
		this.endTime = endTime;
	}
	
	public List<Line2D> getLine2DArrayList() {
		if (nodes==null || nodes.size()<2)
			return new ArrayList<Line2D>();
		
		List<Line2D> ret = new ArrayList<Line2D>();
		for (int i=0; i<nodes.size()-1; i++)
			ret.add(new Line2D.Float(nodes.get(i).x,
					nodes.get(i).y,
					nodes.get(i+1).x,
					nodes.get(i+1).y));
		return ret;
	}
	
	// Node operations :
	/**
	 * This method appends a point to the player's path.
	 * 
	 * @param pt
	 * @param move
	 */
	public void appendNode(Point pt, CSMoveStyle move, int waitTime) {
		CSNode last = nodes.get(nodes.size()-1);
		// Getting the new time (depending on the movestyle of
		// the previous node) :
		double newTime =
			last.getTime()+last.getWaitTime()+GUIParameters.scale*pt.distance(last)/CSConstants.getSpeed(last.getMoveStyle()); 

		// Lines :
		// 1st - point
		// 2nd - new time
		// 3rd - wait time
		// 4rd - move style
		nodes.add(new CSNode(pt.x, pt.y,
				newTime,
				waitTime,
				move));
		if (nodes.size()>1)
			updateNodes(nodes.size()-2);
	}
	// Default 0 waitTime call :
	/**
	 * This method appends a point to the player's path.
	 * 
	 * @param pt
	 * @param move
	 */
	public void appendNode(Point pt, CSMoveStyle move) {
		appendNode(pt, move, 0);
	}
	
	// default RUN move call :
	/**
	 * This method appends a point to the player's path
	 * using the default RUN move style value.
	 * 
	 * @param pt
	 */
	public void appendNode(Point pt) {
		appendNode(pt, CSMoveStyle.RUN);
	}
	
	/**
	 * This method inserts a node after the given index
	 * node.
	 * 
	 * @param index
	 * @param pt
	 * @param move
	 */
	// Inserting AFTER the index :
	public void insertNode(int index, Point pt, CSMoveStyle move) {
		CSNode previous = nodes.get(index);
		
		// Getting the new time (depending on the movestyle of
		// the previous node) :
		double newTime =
			previous.getTime()+previous.getWaitTime()+GUIParameters.scale*pt.distance(previous)/CSConstants.getSpeed(previous.getMoveStyle()); 
		
		// Lines :
		// 1st - point
		// 2nd - new time
		// 3rd - wait time
		// 4rd - move style
		nodes.add(index,new CSNode(pt.x, pt.y,
				newTime,
				0,
				move));
		
		// Updating the tail :
		updateNodes(index+1);
	}
	
	/**
	 * This method inserts a node after the given index
	 * node.
	 * 
	 * @param index
	 * @param node
	 */
	public void insertNode(int index, CSNode node) {
		insertNode(index, node.getPosition(), node.getMoveStyle());
	}
	
	/**
	 * This method deletes the given index node from
	 * the path.
	 * 
	 * @param index
	 */
	public void deleteNode(int index) {
		if (nodes.get(index).getTime()<nodes.get(currentNodeIndex).getTime())
			currentNodeIndex--;
		
		nodes.remove(index);
		
		// Updating the tail :
		updateNodes(index);
	}
	
	/**
	 * This method moves the given index node
	 * to the given point.
	 * 
	 * @param index
	 * @param pt
	 */
	public void moveNode(int index, Point pt) {
		CSNode toMove = nodes.get(index);
		toMove.setPosition(pt);
		
		// Updating the node and the tail :
		updateNodes(index);
	}
	
	/**
	 * This method adds or removes some wait time
	 * and shifts all the Path tail in way to keep
	 * all the further events at the same position
	 * in the map space.
	 * 
	 * @param index
	 * @param style
	 */
	
	public boolean waitUntil(int index, double time) {
		double shift = time - (nodes.get(index).getTime()
								+nodes.get(index).getWaitTime());
		shift = ((int)(100*shift))/100d;
		// If the value is irrealistic, we return false :
		if (shift+nodes.get(index).getWaitTime()<0)
			return false;
		
		nodes.get(index).setWaitTime(nodes.get(index).getWaitTime()+shift);
		
		// Shifting the position nodes :
		for (int i=index+1; i<nodes.size(); i++)
			nodes.get(i).setTime(nodes.get(i).getTime()+shift);
		System.out.println(shift);
		// Shifting the grenade events :
		for (int i=0; i<grenadeEvents.size(); i++)
			if (grenadeEvents.get(i).getStartTime()>nodes.get(index).getTime()
								+nodes.get(index).getWaitTime()-shift)
				grenadeEvents.get(i).setStartTime(grenadeEvents.get(i).getStartTime()+shift);
		System.out.println(losEvents.size());
		// Shifting LOS events :
		for (int i=0; i<losEvents.size(); i++)
			if (losEvents.get(i).getStartTime()>nodes.get(index).getTime()
								+nodes.get(index).getWaitTime()-shift) {
				double waitTime = losEvents.get(i).getEndTime()-losEvents.get(i).getStartTime();
				losEvents.get(i).setStartTime(losEvents.get(i).getStartTime()+shift);
				losEvents.get(i).setEndTime(losEvents.get(i).getStartTime()+waitTime);
				System.out.println(losEvents.get(i).getStartTime());
			}
		
		return true;
	}
	
	public void setNodeMoveStyle(int index, CSMoveStyle style) {
		nodes.get(index).setMoveStyle(style);
		
		// Updating the tail :
		updateNodes(index+1);
	}
	
	public void setNodeWaitTime(int index, double waitTime) {
		nodes.get(index).setWaitTime(waitTime);
		
		// Updating the tail :
		updateNodes(index+1);
	}
	
	// Graphical stuff (node/segment selection) :
	
	/**
	 * Returns the index of the possibly selected index
	 * when the user clicks at the given point.
	 * 
	 * Returns -1 if there is no index in the MIN_SELECTION_DISTANCE
	 * range.
	 * 
	 * @param pt
	 * @return int
	 */
	public int getPossibleSelectedNode(Point pt) {
		for (int i=0;i<nodes.size();i++) {
			if (nodes.get(i).distance(pt)<GUIConstants.MIN_SELECTION_DISTANCE)
				return i;
		}
		return -1;
	}
	
	/**
	 * Returns the index of the possibly selected segment
	 * when the user clicks at the given point.
	 * 
	 * Returns -1 if there is no index in the MIN_SELECTION_DISTANCE
	 * range.
	 * 
	 * @param pt
	 * @return int
	 */
	public int getPossibleSelectedSegment(Point pt) {
		for (int i=0;i<nodes.size()-1;i++) {
			Line2D.Double current = new Line2D.Double(nodes.get(i),nodes.get(i+1));
			if (current.ptLineDist(pt)<GUIConstants.MIN_SELECTION_DISTANCE)
				return i;
		}
		return -1;
	}
	
	// Main job of the class :
	
	/**
	 * This method returns the position of the player
	 * at the given time.
	 * 
	 * @param time
	 * @return Point
	 */
	public Point getPositionAt(double time) {
		if (time<0)
			return null;
		if (time>endTime)
			return null;

		if (time >= currentTime)
			return playUntil(time);
		else if (time < currentTime)
			return rewindUntil(time);
		else return currentPosition;
	}
	
	public double getNextNodeTime() {

		CSNode next;
		CSNode current;
		CSGrenadeEvent gre = null;
		CSLOSEvent los = null;
		
		if (currentNodeIndex < nodes.size()-1)
			if (currentPosition.equals(nodes.get(currentNodeIndex+1).getPosition()))
				currentNodeIndex++;
		
		current = nodes.get(currentNodeIndex);
		next = nodes.get(currentNodeIndex);
		if (currentNodeIndex<nodes.size()-1)
			next = nodes.get(currentNodeIndex+1);
		
		for (int i=0; i<grenadeEvents.size(); i++) {
			if (grenadeEvents.get(i).getStartTime()>currentTime+0.05
					&& grenadeEvents.get(i).getStartTime()<next.getTime()) {
				if (gre == null)
					gre = grenadeEvents.get(i);
				else
					if (grenadeEvents.get(i).getStartTime()<gre.getStartTime())
						gre = grenadeEvents.get(i);
			}
		}
		
		for (int i=0; i<losEvents.size(); i++) {
			if (losEvents.get(i).getStartTime()>currentTime+0.05
					&& losEvents.get(i).getStartTime()<next.getTime()) {
				if (los == null)
					los = losEvents.get(i);
				else
					if (losEvents.get(i).getStartTime()<los.getStartTime())
						los = losEvents.get(i);
			}
		}
		
		if (los == null){
		    if (gre == null){
		        return next.getTime();
		    }
		    return gre.getStartTime();
		}
		else if (gre == null)
			return los.getStartTime();
		else if (gre.getStartTime()<los.getStartTime())
			return gre.getStartTime();
		else
			return los.getStartTime();
	}
	
	public Point getCurrentPosition() {
		return currentPosition;
	}
	
	// Miscellaneous methods
	// Grenade events :
	
	public void addGrenadeEvent(CSGrenadeEvent event) {
		grenadeEvents.add(event);
	}
	
	/**
	 * This method removes the grenade event at the
	 * given time if it exists.
	 * 
	 * @param time
	 */
	public void removeGrenadeEvent(double time) {
		for (int i=0;i<grenadeEvents.size();i++) {
			if (grenadeEvents.get(i).getStartTime()==time) {
				try {
					player.giveGrenade(grenadeEvents.get(i).getType());
				}
				catch (TooMuchGrenades e) {}
				grenadeEvents.remove(i);
				return;
			}
		}
	}
	
	/**
	 * This method returns the grenade event at the
	 * given time.
	 * If it does not exist, it returns null.
	 * 
	 * @param time
	 * @return CSGrenadeEvent
	 */
	public CSGrenadeEvent getGrenadeEventAt(double time) {
		for (int i=grenadeEvents.size()-1;i>=0;i--) {
			if (grenadeEvents.get(i).getStartTime()+CSConstants.GRENADE_TAKE_DELAY >= time
					&& grenadeEvents.get(i).getStartTime()<= time)
				return grenadeEvents.get(i);
		}
		return null;
	}
	
	/**
	 * This method returns all the grenade events at the
	 * given time.
	 * If there is none, it returns an empty ArrayList.
	 * 
	 * @param time
	 * @return a list of {@link CSGrenadeEvent}.
	 */
	public List<CSGrenadeEvent> getAllGrenadeEventsAt(double time) {
		List<CSGrenadeEvent> ret = new ArrayList<CSGrenadeEvent>();
		for (int i=grenadeEvents.size()-1;i>=0;i--) {
			if (grenadeEvents.get(i).getStartTime()+CSConstants.GRENADE_TIMEOUT >= time
					&& grenadeEvents.get(i).getStartTime() <= time)
				ret.add(grenadeEvents.get(i));
		}
		return ret;
	}
	
	/**
	 * This method returns if the player can or not
	 * throw a grenade at the given time.
	 * 
	 * @param time
	 * @return boolean
	 */
	public boolean isGrenadeEventsPossibleAt(double time) {
		for (int i=0;i<grenadeEvents.size();i++) {
			if (grenadeEvents.get(i).getStartTime()+CSConstants.GRENADE_TAKE_DELAY >= time
					&& grenadeEvents.get(i).getStartTime()-CSConstants.GRENADE_TAKE_DELAY <= time)
				return false;
		}
		return true;
	}
	
	public int getFBEventsCount() {
		int ret = 0;
		for (int i=0;i<grenadeEvents.size();i++)
			if (grenadeEvents.get(i).getType() == CSGrenadeType.FB)
				ret++;
		return ret;
	}
	
	public int getHEEventsCount() {
		for (int i=0;i<grenadeEvents.size();i++)
			if (grenadeEvents.get(i).getType() == CSGrenadeType.HE)
				return 1;
		return 0;
	}
	
	public int getSGEventsCount() {
		for (int i=0;i<grenadeEvents.size();i++)
			if (grenadeEvents.get(i).getType() == CSGrenadeType.SG)
				return 1;
		return 0;
	}
	
	// LOS events :
	
	public void addLOSEvent(CSLOSEvent event) {
		losEvents.add(event);
	}
	
	/**
	 * This method removes the LOS event at the
	 * given time if it exists.
	 * 
	 * @param time
	 */
	public void removeLOSEvent(double time) {
		for (int i=0;i<losEvents.size();i++) {
			if (losEvents.get(i).getStartTime()==time) {
				losEvents.remove(i);
				return;
			}
		}
	}
	
	/**
	 * This method returns the LOS event at the
	 * given time.
	 * If it does not exist, it returns null.
	 * 
	 * @param time
	 * @return CSLOSEvent
	 */
	public CSLOSEvent getLOSEventsAt(double time) {
		for (int i=0;i<losEvents.size();i++) {
			if (losEvents.get(i).getStartTime() <= time
					&& losEvents.get(i).getEndTime() >= time)
				return losEvents.get(i);
		}
		return null;
	}
	
	// Private methods for the main job :
	
	private Point playUntil(double time) {
		// Updating the grenade count :
		for (int i=0;i<grenadeEvents.size();i++) {
			if (grenadeEvents.get(i).getStartTime()<time
					&& grenadeEvents.get(i).getStartTime()>=currentTime) {
				try {
					player.throwGrenade(grenadeEvents.get(i).getType());
				}
				catch (NotEnoughGrenades e) {}
			}
		}
		
		// If we have only one node in the path, return it.
		// If we are at the end of the path, we stay at
		// the end of the path whatever the time value
		// asked for is :
		
		if (nodes.size() == 1 || time>=nodes.get(nodes.size()-1).getTime()) {//currentNodeIndex == nodes.size()-1)
			currentNodeIndex = nodes.size()-1;
			currentTime = time;
			currentPosition = nodes.get(currentNodeIndex).getLocation();
			return currentPosition;
		}
		
		// Else, we have to advance until the time asked
		// for :
		while (currentNodeIndex<nodes.size()-1 && nodes.get(currentNodeIndex).getTime()<=time)
			currentNodeIndex++;
		
		currentNodeIndex--; // To reach the node at the left of the time.
		
		CSNode current = nodes.get(currentNodeIndex);
		
		// If we are in the wait time of the node, we return the
		// node's position :
		if (time<=current.getTime()+current.getWaitTime())
			currentPosition = current.getPosition();
		else {
			// Getting the next node :
			CSNode next = nodes.get(currentNodeIndex+1);
			
			// Proportion of the time spent in the segment :
			double prop = (time-current.getTime()-current.getWaitTime())/
			(next.getTime()-current.getTime()-current.getWaitTime());

			// The new position (using the prop) :
			currentPosition = new Point(current.x+(int)Math.round(prop*(next.x-current.x)),
					current.y+(int)Math.round(prop*(next.y-current.y)));
		}
		// Updating the currentTime :
		currentTime = time;
		// Returns the point at the wanted time :
		return currentPosition;
	}
	
	private Point rewindUntil(double time) {
		// Updating the grenade count :
		for (int i=0;i<grenadeEvents.size();i++) {
			if (grenadeEvents.get(i).getStartTime()>=time
					&& grenadeEvents.get(i).getStartTime()<currentTime) {
				try {
					player.giveGrenade(grenadeEvents.get(i).getType());
				}
				catch (TooMuchGrenades e) {}
			}
		}
		
		// If we have only one node in the path, return it.
		// If we are at the beginning of the path, we stay at
		// the beginning of the path whatever the time value
		// asked for is :
		if (nodes.size() == 1 || time <= 0) {
			currentNodeIndex = 0;
			currentTime = time;
			currentPosition = nodes.get(currentNodeIndex).getLocation();
			return currentPosition;
		}
		
		
		// Else, we have to go back to the time asked
		// for :
		while (currentNodeIndex>0 && nodes.get(currentNodeIndex).getTime()>time)
			currentNodeIndex--;
		
		CSNode current = nodes.get(currentNodeIndex);
		
		if (currentNodeIndex == nodes.size()-1) {
			currentTime = time;
			currentPosition = current;
			return currentPosition;
		}
		
		// If we are in the wait time of the node, we return the
		// node's position :
		if (time<=current.getTime()+current.getWaitTime())
			currentPosition = current.getPosition();
		else {
			// Getting the next node :
			CSNode next = nodes.get(currentNodeIndex+1);
			
			// Proportion of the time spent in the segment :
			double prop = (time-current.getTime()-current.getWaitTime())/
			(next.getTime()-current.getTime()-current.getWaitTime());

			// The new position (using the prop) :
			currentPosition = new Point(current.x+(int)Math.round(prop*(next.x-current.x)),
					current.y+(int)Math.round(prop*(next.y-current.y)));
		}
		// Updating the currentTime :
		currentTime = time;
		// Returns the point at the wanted time :
		return currentPosition;
	}
	
	// Private method for updating nodes tail after inserting/deleting
	// a node :
	
	private void updateNodes(final int index1) {
		final int index;
		if (index1 < 1) {
			index = 1;
		} else {
		    index = index1;
		}
		
		for (int i=index; i<nodes.size(); i++) {
			CSNode previous = nodes.get(i-1);
			CSNode current = nodes.get(i);
			// Getting the new time (depending on the movestyle of
			// the previous node) :
			double newTime =
				previous.getTime()+previous.getWaitTime()+GUIParameters.scale*current.distance(previous)/CSConstants.getSpeed(previous.getMoveStyle());
			current.setTime(newTime);
		}
		
		// Updating the Grenades events :
		for (int i=0; i<grenadeEvents.size(); i++)
			grenadeEvents.get(i).setStartPosition(getPositionAt(grenadeEvents.get(i).getStartTime()));
		// Updating the LOS events :
		for (int i=0; i<losEvents.size(); i++)
			losEvents.get(i).setStartPosition(getPositionAt(losEvents.get(i).getStartTime()));
	}

	public CSNode getNode(int index) {
		return nodes.get(index);
	}
	
	public List<CSGrenadeEvent> getGrenadeEvents() {
		return grenadeEvents;
	}

	public List<CSLOSEvent> getLosEvents() {
		return losEvents;
	}

	public double getCurrentTime() {
		return currentTime;
	}
}
